package org.mozhu.file.manager.controller;

import org.apache.commons.io.FileUtils;
import org.mozhu.file.manager.MountDirectoriesConfiguration;
import org.mozhu.file.manager.mount.MountDirectories;
import org.mozhu.file.manager.mount.MountMeta;
import org.mozhu.file.manager.util.CompressUtils;
import org.mozhu.file.manager.util.HttpUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.*;
import java.text.SimpleDateFormat;
import java.util.*;

@Controller
@RequestMapping("/fm")
@EnableConfigurationProperties(MountDirectoriesConfiguration.class)
public class FileManagerController {
    private final static Logger LOGGER = LoggerFactory.getLogger(FileManagerController.class);
    private final static String DATE_FORMAT = "yyyy-MM-dd hh:mm:ss";
    private final static String ROOT = "/";
    private MountDirectories mountDirectories;

    @Autowired
    public void setMountDirectories(MountDirectoriesConfiguration mountDirectoriesConfiguration) {
        this.mountDirectories = new MountDirectories();
        mountDirectoriesConfiguration.getMountDirs().forEach((k, v) -> {
            mountDirectories.mount(k, v);
            LOGGER.info("FileManager Mount Dir[{}] to Path[{}]", v, k);
        });
    }

    @RequestMapping(value = "/list", method = RequestMethod.POST)
    @ResponseBody
    public Object list(@RequestBody Map<String, Object> params) {
        try {
            String path = getString(params, "path");
            Boolean onlyFolders = getBoolean(params, "onlyFolders");
            LOGGER.debug("list path[{}], onlyFolders[{}]");
            if (ROOT.equals(path)) {
                List<Object> listRoot = listRoot();
                return success(listRoot);
            }

            List<Object> resultList = new ArrayList<Object>();
            try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(new File(mountDirectories.getMountDirectory(path).getRealPath()).toPath())) {
                for (Path pathObj : directoryStream) {
                    Map<String, Object> fileObject = toFileObject(pathObj, onlyFolders);
                    if (fileObject != null) {
                        resultList.add(fileObject);
                    }
                }
                return success(resultList);
            }
        } catch (Exception ex) {
            LOGGER.error("list", ex);
            return error(ex.getMessage());
        }
    }

    private Map<String, Object> toFileObject(Path pathObj, boolean onlyFolders) throws IOException {
        SimpleDateFormat dt = new SimpleDateFormat(DATE_FORMAT);
        BasicFileAttributes attrs = Files.readAttributes(pathObj, BasicFileAttributes.class);
        if (onlyFolders && !attrs.isDirectory()) {
            return null;
        }
        Map<String, Object> fileObject = new HashMap<>();
        fileObject.put("name", pathObj.getFileName().toString());
        fileObject.put("rights", getPermissions(pathObj));
        fileObject.put("date", dt.format(new Date(attrs.lastModifiedTime().toMillis())));
        fileObject.put("size", attrs.size());
        fileObject.put("type", attrs.isDirectory() ? "dir" : "file");
        return fileObject;
    }

    private List<Object> listRoot() throws IOException {
        List<Object> resultList = new ArrayList<Object>();
        for (MountMeta mountMeta : mountDirectories.getMountDirectories().values()) {
            String key = mountMeta.getMountPath();
            String realPath = mountMeta.getSourcePath();
            Map<String, Object> fileObject = toFileObject(new File(realPath).toPath(), true);
            if (fileObject != null) {
                fileObject.put("name", key);
                resultList.add(fileObject);
            }
        }
        return resultList;
    }

    @RequestMapping(value = "/createFolder", method = RequestMethod.POST)
    @ResponseBody
    public Object createFolder(@RequestBody Map<String, Object> params) {
        try {
            String path = getString(params, "newPath");
            LOGGER.debug("addFolder path: {} ", path);
            File newDir = new File(mountDirectories.getMountDirectory(path).getRealPath());
            if (!newDir.mkdir()) {
                throw new Exception("Can't create directory: " + newDir.getAbsolutePath());
            }
            return success();
        } catch (Exception e) {
            LOGGER.error("addFolder", e);
            return error(e.getMessage());
        }
    }

    @RequestMapping(value = "/rename", method = RequestMethod.POST)
    @ResponseBody
    public Object rename(@RequestBody Map<String, Object> params) {
        try {
            String item = getString(params, "item");
            String newItemPath = getString(params, "newItemPath");
            LOGGER.debug("rename from: {} to: {}", item, newItemPath);

            File srcFile = new File(mountDirectories.getMountDirectory(item).getRealPath());
            File destFile = new File(mountDirectories.getMountDirectory(newItemPath).getRealPath());
            if (srcFile.isFile()) {
                FileUtils.moveFile(srcFile, destFile);
            } else {
                FileUtils.moveDirectory(srcFile, destFile);
            }
            return success();
        } catch (Exception e) {
            LOGGER.error("rename", e);
            return error(e.getMessage());
        }
    }

    @RequestMapping(value = "/move", method = RequestMethod.POST)
    @ResponseBody
    public Object move(@RequestBody Map<String, Object> params) {
        try {
            String newPath = getString(params, "newPath");
            String realPathDest = mountDirectories.getMountDirectory(newPath).getRealPath();
            List<String> items = getList(params, "items");
            for (String item : items) {
                LOGGER.debug("copy from: {} to: {}", item, newPath);
                File srcFile = new File(mountDirectories.getMountDirectory(item).getRealPath());
                File destFile = new File(realPathDest);
                FileUtils.moveToDirectory(srcFile, destFile, true);
            }
            return success();
        } catch (Exception e) {
            LOGGER.error("copy", e);
            return error(e.getMessage());
        }
    }

    @RequestMapping(value = "/copy", method = RequestMethod.POST)
    @ResponseBody
    public Object copy(@RequestBody Map<String, Object> params) {
        try {
            String newPath = getString(params, "newPath");
            String realPathDest = mountDirectories.getMountDirectory(newPath).getRealPath();
            List<String> items = getList(params, "items");
            for (String item : items) {
                LOGGER.debug("copy from: {} to: {}", item, newPath);
                File srcFile = new File(mountDirectories.getMountDirectory(item).getRealPath());
                File destFile = new File(realPathDest);
                if (srcFile.isFile()) {
                    FileUtils.copyFileToDirectory(srcFile, destFile);
                } else {
                    FileUtils.copyDirectory(srcFile, destFile);
                }
            }
            return success();
        } catch (Exception e) {
            LOGGER.error("copy", e);
            return error(e.getMessage());
        }
    }

    @RequestMapping(value = "/remove", method = RequestMethod.POST)
    @ResponseBody
    public Object remove(@RequestBody Map<String, Object> params) {
        try {
            List<String> items = getList(params, "items");
            for (String item : items) {
                LOGGER.debug("delete {}", item);
                File srcFile = new File(mountDirectories.getMountDirectory(item).getRealPath());
                if (!FileUtils.deleteQuietly(srcFile)) {
                    throw new Exception("Can't delete: " + srcFile.getAbsolutePath());
                }
            }
            return success();
        } catch (Exception e) {
            LOGGER.error("delete", e);
            return error(e.getMessage());
        }
    }

    @RequestMapping(value = "/getContent", method = RequestMethod.POST)
    @ResponseBody
    public Object getContent(@RequestBody Map<String, Object> params) {
        // get content
        try {
            String item = getString(params, "item");
            LOGGER.debug("editFile path: {}", item);

            File srcFile = new File(mountDirectories.getMountDirectory(item).getRealPath());
            String content = FileUtils.readFileToString(srcFile);

            return success(content);
        } catch (Exception e) {
            LOGGER.error("getContent", e);
            return error(e.getMessage());
        }
    }

    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    @ResponseBody
    public Object edit(@RequestBody Map<String, Object> params) {
        // save content
        try {
            String item = getString(params, "item");
            String content = getString(params, "content");
            File srcFile = new File(mountDirectories.getMountDirectory(item).getRealPath());
            FileUtils.writeStringToFile(srcFile, content);
            return success();
        } catch (Exception e) {
            LOGGER.error("saveFile", e);
            return error(e.getMessage());
        }
    }

    @RequestMapping(value = "/changePermissions", method = RequestMethod.POST)
    @ResponseBody
    public Object changePermissions(@RequestBody Map<String, Object> params) {
        try {
            String path = getString(params, "path");
            String perms = getString(params, "perms"); // "653"
            String permsCode = getString(params, "permsCode"); // "rw-r-x-wx"
            boolean recursive = getBoolean(params, "recursive");
            LOGGER.debug("change permissions path: {} perms: {} permsCode: {} recursive: {}", path, perms, permsCode, recursive);
            File f = new File(mountDirectories.getMountDirectory(path).getRealPath());
            setPermissions(f, permsCode, recursive);
            return success();
        } catch (Exception e) {
            LOGGER.error("change permissions", e);
            return error(e.getMessage());
        }
    }

    @RequestMapping(value = "/compress", method = RequestMethod.POST)
    @ResponseBody
    public Object compress(@RequestBody Map<String, Object> params) {
        try {
            List<String> items = getList(params, "items"); // ["/public_html/compressed.zip","xxx"]
            List<String> needCompressPaths = new ArrayList<>();
            for (String item : items) {
                needCompressPaths.add(mountDirectories.getMountDirectory(item).getRealPath());
            }
            String destination = getString(params, "destination"); // "/public_html/backups"
            String compressedFilename = getString(params, "compressedFilename"); // "random-files.zip"
            LOGGER.debug("compress items: {} destination: {} compressedFilename: {}", items, destination, compressedFilename);
            CompressUtils.zip(mountDirectories.getMountDirectory(destination).getRealPath(compressedFilename), needCompressPaths);
            return success();
        } catch (Exception e) {
            LOGGER.error("compress", e);
            return error(e.getMessage());
        }
    }

    @RequestMapping(value = "/extract", method = RequestMethod.POST)
    @ResponseBody
    public Object extract(@RequestBody Map<String, Object> params) {
        try {
            String folderName = getString(params, "folderName"); // "/public_html/compressed.zip"
            String item = getString(params, "item"); // "/public_html/compressed.zip"
            String destination = getString(params, "destination"); // "/public_html/extracted-files"
            LOGGER.debug("extract sourceFile: {} destination: {}", item, destination);
            CompressUtils.unzip(mountDirectories.getMountDirectory(item).getRealPath(), mountDirectories.getMountDirectory(destination + "/" + folderName).getRealPath(), false);
            return success();
        } catch (Exception e) {
            LOGGER.error("extract fail", e);
            return error(e.getMessage());
        }
    }

    @RequestMapping(value = "/downloadMultiple")
    public void downloadMultiple(@RequestParam(name = "items[]") List<String> items, @RequestParam(name = "toFilename") String toFilename, HttpServletResponse response) throws ServletException,
            IOException {
        try {
            List<String> needCompressPaths = new ArrayList<>();
            for (String item : items) {
                needCompressPaths.add(mountDirectories.getMountDirectory(item).getRealPath());
            }
            response.setHeader("Content-Disposition", "inline; filename=\"" + toFilename + "\"");
            CompressUtils.zip(response.getOutputStream(), needCompressPaths);
        } catch (IOException e) {
            LOGGER.debug("Cannot parse multipart request: item.getInputStream");
            throw new ServletException("Cannot parse multipart request: item.getInputStream", e);
        } catch (Exception e) {
            LOGGER.debug("Cannot write file");
            throw new ServletException("Cannot write file", e);
        }
    }

    @RequestMapping(value = "/downloadFile", method = RequestMethod.GET)
    public void downloadFile(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // [fileManagerConfig.downloadFileUrl]?action=download&path=/public_html/image.jpg
        String path = request.getParameter("path");
        LOGGER.debug("download: {}", path);

        File file = new File(mountDirectories.getMountDirectory(path).getRealPath());

        if (!file.isFile()) {
            // if not a file, it is a folder, show this error.
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "Resource Not Found");
            return;
        }

        response.setHeader("Content-Type", getMimeType(request, file.getName()));
        response.setHeader("Content-Length", String.valueOf(file.length()));
        response.setHeader("Content-Disposition", HttpUtils.getContentDisposition(request, file.getName()));

        try (FileInputStream input = new FileInputStream(file); BufferedOutputStream output = new BufferedOutputStream(response.getOutputStream())) {
            byte[] buffer = new byte[8192];
            for (int length = 0; (length = input.read(buffer)) > 0; ) {
                output.write(buffer, 0, length);
            }
        } catch (Exception e) {
            LOGGER.error("Get file[" + path + "] error", e);
        }
    }

    private String getMimeType(HttpServletRequest request, String name) {
        String mimeType = request.getServletContext().getMimeType(name);
        return mimeType != null ? mimeType : "text/plain";
    }

    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    @ResponseBody
    public Object upload(MultipartHttpServletRequest multiRequest) throws ServletException {
        try {
            // URL: $config.uploadUrl, Method: POST, Content-Type:
            // multipart/form-data
            // Unlimited file upload, each item will be enumerated as file-1,
            // file-2, etc.
            // [$config.uploadUrl]?destination=/public_html/image.jpg&file-1={..}&file-2={...}
            try {
                String destination = multiRequest.getParameter("destination");
                Assert.notNull(destination, "destination should not be null");
                Iterator<String> iter = multiRequest.getFileNames();
                while (iter.hasNext()) {
                    MultipartFile file = multiRequest.getFile(iter.next().toString());
                    if (file != null) {
                        String path = mountDirectories.getMountDirectory(destination).getRealPath(file.getOriginalFilename());
                        // 上传
                        file.transferTo(new File(path));
                    }
                }
                return success();
            } catch (IOException e) {
                LOGGER.debug("Cannot parse multipart request: item.getInputStream");
                throw new ServletException("Cannot parse multipart request: item.getInputStream", e);
            } catch (Exception e) {
                LOGGER.debug("Cannot write file");
                throw new ServletException("Cannot write file", e);
            }
        } catch (Exception e) {
            LOGGER.error("extract", e);
            return error(e.getMessage());
        }

    }

    private Boolean getBoolean(Map<String, Object> params, String key) {
        String value = (String) params.get(key);
        if (value == null) {
            return false;
        }
        return Boolean.valueOf(value);
    }

    @SuppressWarnings("unchecked")
    private List<String> getList(Map<String, Object> params, String key) {
        return (List<String>) params.get(key);
    }

    private String getString(Map<String, Object> params, String key) {
        return (String) params.get(key);
    }

    private String getPermissions(Path path) throws IOException {
        if (!isSupportPosix(path)) {
            // Winsows not support posix
            return path.toFile().isDirectory() ? "drwxrwxrwx" : "-rwxrwxrwx";
        }
        // http://www.programcreek.com/java-api-examples/index.php?api=java.nio.file.attribute.PosixFileAttributes
        PosixFileAttributeView fileAttributeView = Files.getFileAttributeView(path, PosixFileAttributeView.class);
        PosixFileAttributes readAttributes = fileAttributeView.readAttributes();
        Set<PosixFilePermission> permissions = readAttributes.permissions();
        return PosixFilePermissions.toString(permissions);
    }

    private String setPermissions(File file, String permsCode, boolean recursive) throws IOException {
        if (!isSupportPosix(file.toPath())) {
            // Winsows not support posix
            return permsCode;
        }
        // http://www.programcreek.com/java-api-examples/index.php?api=java.nio.file.attribute.PosixFileAttributes
        PosixFileAttributeView fileAttributeView = Files.getFileAttributeView(file.toPath(), PosixFileAttributeView.class);
        fileAttributeView.setPermissions(PosixFilePermissions.fromString(permsCode));
        if (file.isDirectory() && recursive && file.listFiles() != null) {
            for (File f : file.listFiles()) {
                setPermissions(f, permsCode, recursive);
            }
        }
        return permsCode;
    }

    private boolean isSupportPosix(Path path) {
        FileSystem fileSystem = path.getFileSystem();
        Set<String> supportedViews = fileSystem.supportedFileAttributeViews();
        return supportedViews.contains("posix");
    }

    // { "result": { "success": false, "error": "msg" } }
    private Object error(String msg) {
        Map<String, Object> result = new HashMap<>();
        result.put("success", false);
        result.put("error", msg);
        return map("result", result);
    }

    // { "result": { "xxx": xxx, "yyy": yyy } }
    private Object success(Object result) {
        return map("result", result);
    }

    // { "result": { "success": true, "error": null } }
    private Object success() {
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("error", null);
        return map("result", result);
    }

    private Map<String, Object> map(String key, Object value) {
        Map<String, Object> map = new HashMap<>();
        map.put(key, value);
        return map;
    }
}
